package cheme.ProcessModel;

import java.beans.PropertyChangeEvent;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.StringTokenizer;

import org.eclipse.draw2d.geometry.Rectangle;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import cheme.ProcessModel.Annotations.IAnnotation;
import cheme.ProcessModel.Equations.Equation;
import cheme.ProcessModel.Equations.EquationField;
import cheme.ProcessModel.Equations.Equations;
import cheme.ProcessModel.Interpreter.Interpreter;
import cheme.ProcessModel.ProcessUnits.*;
import cheme.ProcessModel.ProcessUnits.Streams.Stream;
import cheme.ProcessModel.ProcessUnits.Streams.Tags.ITagComponent;
import cheme.ProcessModel.ProcessUnits.Streams.Tags.Tag;
import cheme.ProcessModel.Visitors.IModelVisitor;
import cheme.ProcessModel.ProcessUnits.Streams.*;
import cheme.ProcessModel.ProcessUnits.Streams.Tags.*;
import cheme.ProcessModel.Subprocess.Subprocess;

/*
 * The root element of the chemical process model. This element contains an
 * Equations element initially and may have process units added to it (Streams,
 * in turn, are children of process units only, and Tags are children of
 * Streams. Equations are children of the Equations EquationList element).
 */

public class ProcessModel extends AbstractParent<IChild> implements IProcessModel, /*IEquationParent,*/ Serializable
{
	/*
	 * Serialization version identifier. (Set to default for now)
	 */
	
	private static final long serialVersionUID = 1L;
	
	/*
	 * Default indices of this model element's primary children.
	 */

	public static final int EQUATIONS_INDEX = 0;
	public static final int CONSOLE_VIEW_INDEX = 1;
	public static final int TOOL_TIP_MODEL = 2;
	
	public static final int NEXT_INDEX = 2+1;
	
	// PA experiment -- Contents model
	private static String me = " cheme.ProcessModel.ProcessModel";
	private Rectangle constraint;
	
	/*
	 * Store the degree to which this element presentation is scaled prior to
	 * painting on the canvas.
	 */
	
	private transient double zoom = 1.0;
	
	private ConsoleModel cosoleObject = new ConsoleModel();
	
	public double getZoom()
	{
		return zoom;
	}

	public void setZoom(double zoom)
	{
		this.zoom = zoom;
	}
	
	public ProcessModel()
	{
		this((IModelElement)null);
	}
	
	public ProcessModel(IModelElement parent)
	{
		super((IModelElement)null);
		add((Equations)ModelEnv.getInstanceFor(this).createModelElement(ProcessModelConstants.EQUATIONS, this), EQUATIONS_INDEX);
		
		
		add((ConsoleView)ModelEnv.getInstanceFor(this).createModelElement(ProcessModelConstants.CONSOLE_VIEW, this), CONSOLE_VIEW_INDEX);
		add((TooltipModel)ModelEnv.getInstanceFor(this).createModelElement(ProcessModelConstants.TOOLTIPMODEL, this), TOOL_TIP_MODEL);
		
		// setting the process model as model element
		// this will in turn set the cvm object in consoleobject
		cosoleObject.setModelElement(this);
		
		// PA experiment for file saving
		if (parent instanceof ProcessModel)
		{
			// This should behave as a copy constructor
			// Copy the contents of parent into this
			ProcessModel temp = (ProcessModel)parent;
			List<IChild> childList = temp.getChildren();
			// skip first three elements
			Iterator<IChild> iterator = childList.iterator();
			iterator.next();// 1
			iterator.next(); // 2
			iterator.next(); // 3
			while(iterator.hasNext())
			{
				IChild current = iterator.next();
				this.add(current);				
			}
			
		}
		ModelEnv.getInstanceFor(getRoot()).hookModelElement(this.getRoot());
	}
	
	public ProcessModel(Document xmlDoc)
	{
		super((IModelElement)null);

		//add equation editor, tooltip, and feedback
		add((Equations)ModelEnv.getInstanceFor(this).createModelElement(ProcessModelConstants.EQUATIONS, this), EQUATIONS_INDEX);
		add((ConsoleView)ModelEnv.getInstanceFor(this).createModelElement(ProcessModelConstants.CONSOLE_VIEW, this), CONSOLE_VIEW_INDEX);
		add((TooltipModel)ModelEnv.getInstanceFor(this).createModelElement(ProcessModelConstants.TOOLTIPMODEL, this), TOOL_TIP_MODEL);
		
		// setting the process model as model element
		// this will in turn set the cvm object in consoleobject
		cosoleObject.setModelElement(this);
		//vars used to keep track of varous information
		List<String> finishedEquations = new ArrayList <String>();
		HashMap<Integer, AbstractStream> streams = new HashMap<Integer, AbstractStream>();
		HashMap<Integer, List<Integer>> outStreamMap = new HashMap<Integer, List<Integer>>();
		HashMap<Integer, List<Integer>> inStreamMap = new HashMap<Integer, List<Integer>>();
		Element e = (Element)xmlDoc.getElementsByTagName("Children").item(0);
		NodeList modelChildren = e.getChildNodes();
		
		//loop through all children, creating objects for each element
		for(int i = 0; i < modelChildren.getLength(); i++)
		{
			Element currentNode = (Element)modelChildren.item(i);
			String elementName = currentNode.getTagName();
			if(elementName == "Equations")
			{
				Element equationChildren = (Element)currentNode.getElementsByTagName("Children").item(0);
			 	Element equationList = (Element)equationChildren.getElementsByTagName("EquationList").item(0);
			 	Element equationListChildren = (Element)equationList.getElementsByTagName("Children").item(0);
			 	NodeList equations = equationListChildren.getElementsByTagName("Equation");
			 	for(int j = 0; j < equations.getLength() - 1; j++)
			 	{
			 		try
			 		{
			 			String equationValue = equations.item(j).getFirstChild().getNodeValue();
			 			
			 			//we can't create equations in the application until everything has been drawn
				 		//therefore, just stick the equations in this array and add them when we've
				 		//finished the loop
				 		finishedEquations.add(equationValue);
			 		}
			 		catch(Exception ex)
			 		{
			 			continue;
			 		}
			 		
			 	}
			}
			
			//else, we're dealing with some sort of process unit
			else
			{
				
				//create the appropriate process unit
				int instanceId = 0;
				if(elementName == "Mixer")
				{
					Mixer m = new Mixer(currentNode);
					instanceId = m.getInstanceId();
					this.add(m);
				}
				else if(elementName == "Separator")
				{
					Separator s = new Separator(currentNode);
					instanceId = s.getInstanceId();
					this.add(s);
				}
				else if(elementName == "TerminalSource")
				{
					TerminalSource t = new TerminalSource(currentNode);
					instanceId = t.getInstanceId();
					this.add(t);
				}
				else if(elementName == "TerminalSink")
				{
					TerminalSink t = new TerminalSink(currentNode);
					instanceId = t.getInstanceId();
					this.add(t);
				}
				else if(elementName.compareTo("SplitJoin") == 0)
				{
					SplitJoin sj = new SplitJoin(currentNode);
					instanceId = sj.getInstanceId();
					this.add(sj);
				}
				else if(elementName.compareTo("SubProcess") == 0)
				{
					Subprocess sp = new Subprocess(currentNode);
					instanceId = sp.getInstanceId();
					this.add(sp);
				}
				else
				{
					continue;
				}
				
				//loop through "inStreams" and "outStreams", adding to the complete list
				//of streams for this diagram
				Element inStreams = (Element)currentNode.getElementsByTagName("InStreams").item(0);
				Element outStreams = (Element)currentNode.getElementsByTagName("OutStreams").item(0);
				
				//pull from the inStreams
				NodeList childNodes = inStreams.getChildNodes();
				for(int j = 0; j < childNodes.getLength(); j++)
				{
					Stream s;
					try
					{
						s = new Stream(this, (Element)childNodes.item(j));
					}
					catch(Exception ex)
					{
						continue;
					}
					
					if(!streams.containsKey(s.getInstanceId()))
					{
						streams.put(s.getInstanceId(), s);
					}
					else
					{
						s = (Stream)streams.get(s.getInstanceId());
						
						//if the stream is attached to a TerminalSink, we need to set
						//the stream's terminal source attribute to true
						if(elementName == "TerminalSink")
						{
							s.setTerminalStream(true);
						}
					}
					
					if(!inStreamMap.containsKey(instanceId))
					{
						List<Integer> l = new ArrayList<Integer>();
						inStreamMap.put(instanceId, l);
					}
					inStreamMap.get(instanceId).add(s.getInstanceId());
				}
				
				//pull form the outStreams
				childNodes = outStreams.getChildNodes();
				for(int j = 0; j < childNodes.getLength(); j++)
				{
					Stream s;
					try
					{
						s = new Stream(this, (Element)childNodes.item(j));
					}
					catch(Exception ex)
					{
						continue;
					}
					
					if(!streams.containsKey(s.getInstanceId()))
					{
						streams.put(s.getInstanceId(), s);
					}
					else
					{
						s = (Stream)streams.get(s.getInstanceId());
						
						//if the stream is attached to a TerminalSink, we need to set
						//the stream's terminal source attribute to true
						if(elementName == "TerminalSink")
						{
							s.setTerminalStream(true);
						}
					}
					
					if(!outStreamMap.containsKey(instanceId))
					{
						List<Integer> l = new ArrayList<Integer>();
						outStreamMap.put(instanceId, l);
					}
					outStreamMap.get(instanceId).add(s.getInstanceId());
				}
			}
		}
		
		//with the XML fully parsed, we can now go back through our Objects and
		//add the appropriate streams
		List<IChild> children = this.getChildren();
		Iterator it = children.iterator();
		while(it.hasNext())
		{
			Object next = it.next();
			if(next instanceof ProcessUnit)
			{
				ProcessUnit pu = (ProcessUnit)next;
				
				//check outgoing streams
				if(outStreamMap.containsKey(pu.getInstanceId()))
				{
					Iterator it2 = outStreamMap.get(pu.getInstanceId()).iterator();
					while(it2.hasNext())
					{
						int instanceId = (Integer)it2.next();
						streams.get(instanceId).setSource(pu);
						streams.get(instanceId).attachSource();
					}
				}
				
				//check incoming streams
				if(inStreamMap.containsKey(pu.getInstanceId()))
				{
					Iterator it2 = inStreamMap.get(pu.getInstanceId()).iterator();
					while(it2.hasNext())
					{
						int instanceId = (Integer)it2.next();
						streams.get(instanceId).setSink(pu);
						streams.get(instanceId).attachSink();
					}
				}
			}
		}
		
		//with everything on the screen, we can now finally re-add the equations
		//remove the equation that gets created by default
		this.remove((Equation)this.getEquations().getEquationList().get(0));
		
		//begin loop
		it = finishedEquations.iterator();
		while(it.hasNext())
		{
			String equationValue = (String)it.next();
			String[] pieces = equationValue.split("\\|");
			Equation currentEquation = new Equation(this, pieces[0]);
			currentEquation.setInitial(false);
			this.add(currentEquation);
			for(int i = 1; i < pieces.length; i++)
			{
				EquationField token = new EquationField(currentEquation, pieces[i].trim());
				currentEquation.getChildren().add(token);
			}
		}
		
		//add empty equation for users to add new equations
		this.add(new Equation(this));
		
		ModelEnv.getInstanceFor(getRoot()).hookModelElement(this.getRoot());
		Interpreter interpreter = ModelEnv.getInstanceFor(this).getInterpreter();
		interpreter.initiateInterpretation();
		
	}
	
//	private void writeObject(java.io.ObjectOutputStream out) throws IOException
//	{
//	}
	
	private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException
	{
		zoom = 1.0;
		ModelEnv.getInstanceFor(this).hookModelElement(this);
	}
	
	@Override
	public void addAnnotation(IAnnotation annotation)
	{
		getEquations().addAnnotation(annotation);
	}
	
	@Override
	public void removeAnnotation(IAnnotation annotation)
	{
		getEquations().removeAnnotation(annotation);
	}
	
	/**
	 * @return the equations
	 */
	public Equations getEquations()
	{
		return (Equations)get(EQUATIONS_INDEX);
	}
	
	@Override
	protected void unhookChild(IChild child)
	{
		ModelEnv.getInstanceFor(this).hookModelElement(child);
		super.unhookChild(child);
	}
	
	// JB: Experiment
	public void add(IChild child)
	{
		addInternal(child);
		
		//ChemePropertyChangeEvent evt = 
		//	new ChemePropertyChangeEvent(this, IPropertyAware.P_INTERPRETABLE, null, pu, 
		//								new ChemePropertyChangeEvent(this, IPropertyAware.P_CHILD, null, pu));

		firePropertyChange(IPropertyAware.P_CHILD, null, child);
		//firePropertyChange(IPropertyAware.P_INTERPRETABLE, null, this);
	}
	
	public void remove(IChild child)
	{
		removeInternal(child);		
		firePropertyChange(IPropertyAware.P_CHILD, child, null);
		// PA uncommented this - For stream deletion handling.
		firePropertyChange(IPropertyAware.P_INTERPRETABLE, child, this);
	
	}


	//	 JB: Experiment
	
	public void add(Equation equation)
	{
		 getEquations().add(equation);
	}
	
	public void remove(Equation equation)
	{
		 getEquations().remove(equation);
	}
	
	@Override
	protected String getToStringSeparator()
	{
		return "\n------------------------------------\n";
	}
	
	public Object accept(IModelVisitor modelVisitor)
	{
		return modelVisitor.visit(this);
	}
	
	// PA experiment -- Contents model
	public Rectangle getConstraint()
	{
		//SimpleLogger.log(me + getSet, me + ".getConstraint()");
		
		return constraint;
	}

	public void setConstraint(Rectangle rect)
	{
		String getSet = "-getSet";
		SimpleLogger.log(me + getSet, me + ".setConstraint(rect)");
		
		constraint = rect;
		firePropertyChange(P_CONSTRAINT, null, constraint);
	}
	
	public String toXml()
	{
		String xml = super.toXml("ChemProvModel");
		Document doc = (Document)XmlHelper.xmlFromString(xml);
		Element e1 = doc.getDocumentElement();
		e1.setAttribute("version", "1.0");
		xml = XmlHelper.xmlToString(doc);
		return xml;
	}
	
	public ConsoleModel getCosoleObject()
	{
		 return cosoleObject;
	}
}
